package com.smdk.dsminio.service.impl;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil;
import cn.hutool.log.StaticLog;
import com.smdk.dsminio.common.CommonVariable;
import com.smdk.dsminio.redis.RedisUtil;
import com.smdk.dsminio.service.FileStorageService;
import com.smdk.dsminio.utils.ConsistentHashingWithoutVirtualNode;
import com.smdk.dsminio.utils.Constants;
import com.smdk.dsminio.utils.FileMD5Util;
import com.smdk.dsminio.vo.MultipartFileParam;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * Created by 神秘的凯 on 2020/11/20.
 */
@Service
public class FileStorageServiceImpl implements FileStorageService {

    @Autowired
    private RedisUtil REDISUTILS;


    //这个必须与前端设定的值一致
    @Value("${breakpoint.upload.chunkSize}")
    private long CHUNK_SIZE;

    @Value("${spring.application.name}")
    private String applicationServerName;


    @Override
    public void init() {
        //初始化文件夹
        try {
            StaticLog.info("服务器虚拟节点初始化开始......");
            ConsistentHashingWithoutVirtualNode.init(new  String[]{applicationServerName});
            SortedMap<Integer, String> fileDirMap = ConsistentHashingWithoutVirtualNode.virtualNodes;
            for (String fileDirName : fileDirMap.values()) {
                FileUtil.mkdir( CommonVariable.ossStorageFilePath + fileDirName.replace("&&", "/"));
            }
            StaticLog.info("服务器虚拟节点初始化完毕......");
            //临时目录
            FileUtil.mkdir(CommonVariable.ossWorkTempFilePath);
            StaticLog.info("临时目录初始化完毕......");
            //水印目录
            FileUtil.mkdir(CommonVariable.ossWorkTempWatermarkFilePath);
            StaticLog.info("水印目录初始化完毕......");
            //日志路径
            FileUtil.mkdir(CommonVariable.ossLogsPath);
            StaticLog.info("日志初始化完毕......");
        } catch (Exception e) {
            StaticLog.error("初始化root文件夹失败。", e);
        }
    }

    @Override
    public boolean uploadFileRandomAccessFile(MultipartFileParam param) throws IOException {
        File file= Base64.decodeToFile(param.getFileBaseStr(),FileUtil.createTempFile(new File(CommonVariable.ossWorkTempFilePath),true));
        String fileName = file.getName();
        //hash算法进行文件存储节点计算
        String tempDirPath = CommonVariable.ossStorageFilePath+ConsistentHashingWithoutVirtualNode.getServer(param.getMd5()).replace("&&","/");
        String tempFileName = fileName + "_tmp";
        File tmpDir = new File(tempDirPath);
        File tmpFile = new File(tempDirPath, tempFileName);
        if (!tmpDir.exists()) {
            tmpDir.mkdirs();
        }
        RandomAccessFile accessTmpFile = new RandomAccessFile(tmpFile, "rw");
        long offset = CHUNK_SIZE * param.getChunk();
        //定位到该分片的偏移量
        accessTmpFile.seek(offset);
        //写入该分片数据
        accessTmpFile.write(FileUtil.readBytes(file));
        // 释放
        accessTmpFile.close();

        boolean isOk = checkAndSetUploadProgress(param, tempDirPath);
        if (isOk) {
            boolean flag = renameFile(tmpFile, fileName);
            System.out.println("upload complete !!" + flag + " name=" + fileName);
        }
        return isOk;
    }

    @Override
    public synchronized boolean uploadFileByMappedByteBuffer(MultipartFileParam param) throws IOException {
        File file= Base64.decodeToFile(param.getFileBaseStr(),FileUtil.createTempFile(new File(CommonVariable.ossWorkTempFilePath),true));
        //获取文件名称
        String fileName = param.getFileName();
        //获取文件上传路径与文件名
        //hash算法进行文件存储节点计算
        String uploadDirPath = CommonVariable.ossStorageFilePath+ConsistentHashingWithoutVirtualNode.getServer(param.getMd5()).replace("&&","/");
        //临时文件名
        String tempFileName = fileName + "_tmp";
        //判断本地文件是否存在 不存在就创建
        File tmpDir = new File(uploadDirPath);
        //临时文件
        File tmpFile = new File(uploadDirPath, tempFileName);
        if (!tmpDir.exists()) {
            tmpDir.mkdirs();
        }
        //创建一个随机的读写类（该类通过自带的读写流非常的快速 并且可以进行大文件文本定位 日志追加 断点续写）
        RandomAccessFile tempRaf = new RandomAccessFile(tmpFile, "rw");
        FileChannel fileChannel = tempRaf.getChannel();

        //写入该分片数据
        long offset = CHUNK_SIZE * param.getChunk();
        byte[] fileData = FileUtil.readBytes(file);
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);
        mappedByteBuffer.put(fileData);
        //检测有没有别的线程正在读取
        FileMD5Util.freedMappedByteBuffer(mappedByteBuffer);
        // 释放
        fileChannel.close();
        //清除
        tempRaf.close();
        //检查并修改文件上传进度
        boolean isOk = checkAndSetUploadProgress(param, uploadDirPath);
        if (isOk) {
            isOk = renameFile(tmpFile, fileName);
            System.out.println("upload complete !!" + isOk + " name=" + fileName);
        }
        //删除临时文件
        FileUtil.del(file);
        return isOk;
    }

    /**
     * 检查并修改文件上传进度
     *
     * @param param
     * @param uploadDirPath
     * @return
     * @throws IOException
     */
    private boolean checkAndSetUploadProgress(MultipartFileParam param, String uploadDirPath) throws IOException {
        String fileName = param.getFileName();
        File confFile = new File(uploadDirPath, fileName + ".conf");
        RandomAccessFile accessConfFile = new RandomAccessFile(confFile, "rw");
        //把该分段标记为 true 表示完成
        StaticLog.info("set part " + param.getChunk() + " complete");
        accessConfFile.setLength(param.getChunks());
        accessConfFile.seek(param.getChunk());
        accessConfFile.write(Byte.MAX_VALUE);

        //completeList 检查是否全部完成,如果数组里是否全部都是(全部分片都成功上传)
        byte[] completeList = FileUtil.readBytes(confFile);
        byte isComplete = Byte.MAX_VALUE;
        for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) {
            //与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE
            isComplete = (byte) (isComplete & completeList[i]);
            System.out.println("check part " + i + " complete?:" + completeList[i]);
        }
        //关闭访问流
        accessConfFile.close();
        if (isComplete == Byte.MAX_VALUE) {
            //全部上传完成
            REDISUTILS.hset(Constants.FILE_UPLOAD_STATUS, param.getMd5(), "true");
            REDISUTILS.set(Constants.FILE_MD5_KEY + param.getMd5(), uploadDirPath + "/" + fileName);
            return true;
        } else {
            if (!REDISUTILS.hexists(Constants.FILE_UPLOAD_STATUS, param.getMd5())) {
                REDISUTILS.hset(Constants.FILE_UPLOAD_STATUS, param.getMd5(), "false");
            }
            if (!REDISUTILS.exists(Constants.FILE_MD5_KEY + param.getMd5())) {
                REDISUTILS.set(Constants.FILE_MD5_KEY + param.getMd5(), uploadDirPath + "/" + fileName + ".conf");
            }
            return false;
        }
    }

    /**
     * 文件重命名
     *
     * @param toBeRenamed   将要修改名字的文件
     * @param toFileNewName 新的名字
     * @return
     */
    public boolean renameFile(File toBeRenamed, String toFileNewName) {
        //检查要重命名的文件是否存在，是否是文件
        if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {
            StaticLog.info("File does not exist: " + toBeRenamed.getName());
            return false;
        }
        String p = toBeRenamed.getParent();
        File newFile = new File(p + File.separatorChar + toFileNewName);
        //修改文件名
        return toBeRenamed.renameTo(newFile);
    }

}
